KVS — DSL для Алкотрейдинга! Сегодня мы попытаемся описать жутко неэффективный, но безумно-понятный алгоритм сборки Стакана ордер буков для того, чтобы продемонстрировать убийственную простоту и понятность каждому эрланг-быдлокодеру, который решил срубить баблишка на Алкотрейдиинге! Кто не знает Эрланг — вы можете всячески демонстрировать свое превосходство, но Эрланг в не HTF сфере отлично рулит, для биткоин- Алкотрейдинга самое оно. Я решил показать, что KVS понятен и прост настолько насколько может быть понятно и простой BDB. В этом примере нет QLC запросов (аналога джойнов в SQL), тут используется только операция взятие элемента из стрима по его индексу. При сборке буков мы будем использовать io таблицу локальных идентификаторов ордеров с heap дисциплиной (хранятся освобожденные) а также order таблицу ордеров для мапинга локальных и внешний айдишников. Обе таблицы для всех ордеров. Размер этих таблиц а рантайме около 200 и 3000. -record(io, { uid=[], id=[], sym=[] }). -record(order, { uid=[], local_id=[], sym=[] }). > kvs:all(io). > kvs:all(order). Можно было сделать, чтобы метаинформация генерировалась по ходу трафика или автоматически при старте, пока я сделал, чтобы она была в файле, а символы прописывались как в конфиге. Вот пример того, что в будущем будет генерироваться: metainfo() -> #schema { name = trading, tables = [ #table { name = io, fields = record_info(fields, io), keys=[sym,id], copy_type=ram_copies }, #table { name = order, fields = record_info(fields, order), copy_type=ram_copies }, #table { name = bitmex_btc_usd_swap, fields = record_info(fields, tick), keys=[id,price], copy_type=ram_copies }, #table { name = bitmex_coin_future, fields = record_info(fields, tick), keys=[id,price], copy_type=ram_copies }, #table { name = bitmex_dash_futute, fields = record_info(fields, tick), keys=[id,price], copy_type=ram_copies }, #table { name = bitmex_eth_future, fields = record_info(fields, tick), keys=[id,price], copy_type=ram_copies }, #table { name = 'gdax_btc_usd', fields = record_info(fields, tick), keys=[id,price], copy_type=ram_copies }, #table { name = 'gdax_btc_eur', fields = record_info(fields, tick), keys=[id,price], copy_type=ram_copies }, #table { name = 'gdax_btc_gbp', fields = record_info(fields, tick), keys=[id,price], copy_type=ram_copies }, #table { name = 'gdax_eth_btc', fields = record_info(fields, tick), keys=[id,price], copy_type=ram_copies }, #table { name = 'gdax_eth_usd', fields = record_info(fields, tick), keys=[id,price], copy_type=ram_copies } ] }. Для каждого инструмента храниться своя ордер бука. Проверить какое количество инструментов трекает система от разных площадок можно так: > book:instruments(). [bitmex_btc_usd_swap,bitmex_coin_future,bitmex_dash_futute, bitmex_eth_future,gdax_btc_usd,gdax_btc_eur,gdax_btc_gbp, gdax_eth_btc,gdax_eth_usd] Посмотреть на ордербуку: > book:print(gdax_eth_usd). Price Size ----- ----------- 17.2 -63.525569 16.55 -773.89671 16.49 -193.229001 16.47 -596.371001 16.46 -1326.826 16.43 -388.725 16.4 -542.978 16.39 -335.531483 16.38 121.982 16.36 122.26 16.31 105.91549 16.3 556.308115 16.28 556.99154 Depth: 13 Total: -2757.625619 ok > kvs:all(bitmex_btc_usd_swap). [{bitmex_btc_usd_swap,8799877365,"122635000000",1545, -50000000000,bitmex_btc_usd_swap,ask}, {bitmex_btc_usd_swap,8799875236,"124764000000",1468, -650000000000,bitmex_btc_usd_swap,ask}, {bitmex_btc_usd_swap,8799878320,"121680000000",1567, 550000000000,bitmex_btc_usd_swap,bid}, В системе могут происходить только 3 вида событий для ордеров и 2 вида событий для трейдов, все они по разному сигнализируются дополнительным знаком "-" для ask ордеров. 10:40:60.871 +762 1207.95 0.01 — bid order 10:40:60.850 -184 0 — cancel order 10:40:60.871 -762 1207.95 0.01 — ask order 13:37:37.707 1188.48 -472.0 — ask trade happened at price level 13:37:38.611 1188.43 593.0 — bid trade happened at price level Все это пишется в каталоги такой структуры: priv ├── bitmex │ ├── order │ │ └── 2017-3-1 │ └── trade │ └── 2017-3-1 └── gdax ├── order │ └── 2017-3-1 └── trade └── 2017-3-1 Задание. Итак начнем с того, что сэмулируем работу heap аллокатора имея одну индексированную колонку. Поскольку тут будет всех держать то индексировать будем по буке, а список свободных ячеек индексированного стрима. При каждой аллокации нового локального идентификатора для буки мы будем сверятся с индексированной колонкой на предмет хотя бы одного свободного номера (который будем отдавать), в противном случае будем увеличивать счетчик идентификаторов для таблицы — kvs:next_id(Symbol,1). То, что здесь сказано на Erlang выглядит так: free({Sym,UID}) -> kvs:put(#io{uid=UID,id=UID,sym=Sym}). alloc(Symbol) -> case kvs:index(io,sym,Symbol) of [] -> kvs:next_id(Symbol,1); [#io{uid=Key,sym=Sym,id=UID}|_] -> kvs:delete(io,Key), UID end. Сама ордер бука получает только дельты для определенного прайс левела (т.е после чендж ордеров все сюда должно уже нормализированое попасть). -record(tick, { uid=[], price=[], id=[], size=[], sym=[], side=[] }). Каждый пришедший ордер полюбому ложим в таблицу ордеров с новым аллоцированым id. Ищется запись прайслевела этой буки, и если она есть, то апдетится суммированным значением. В лог скидываем абсолюлное значение ордера, а направление суммы переходит к локальному номеру ордера в нашей системе типа такого 19:35:13.513 -31 1224.46 0.25: add(#tick{sym=[]}) -> []; add(#tick{price=P,size=S,sym=Sym,id=O,side=Side}=T) -> UID = book:alloc(Sym), kvs:put(#order{uid=O,local_id=UID,sym=Sym,price=P}), case kvs:index(Sym,price,P) of [{_,_,_,_,XS,_,_}=X] -> kvs:put(setelement(#tick.size,X,XS+S)), [UID,P,abs(S),Side]; [] -> kvs:put(setelement(1, setelement(#tick.size, setelement(#tick.uid,T,O),S),Sym)), [UID,P,abs(S),Side] end. Для удаления ордера из системы, перед нахождения самой строки прайслевела, делается один лукап в таблицу ордеров, чтобы во-первых освободить локальный идентификатор этого ордера из таблицы io, а во-вторых, чтобы удалить ордер из таблицы order потом. Находим, и если есть, изменяем прайслевел у ордербуки на дельту цены во внутреннем ордере tick. В лог скидываем просто номер ордера, возле которого будет стоять ноль (удадаление одрера). del(#tick{sym=[]}) -> []; del(#tick{id=O,size=S,sym=Sym}=Tick) -> case kvs:get(order,O) of {error,_} -> []; {ok,#order{uid=O,local_id=UID,price=Price}} -> book:free({Sym,UID}), kvs:delete(order,O), case kvs:index(Sym,price,Price) of [X] -> kvs:put(setelement(#tick.size,X, element(#tick.size,X)+S)), [UID]; [] -> [UID] end end. Посмотреть текущий пул свободных идентификаторов, которые можно использовать для именования локальных ордеров можно посмотреть так: > kvs:all(io). [{io,41,41,gdax_eth_usd}, {io,170,170,gdax_btc_eur}, {io,172,172,gdax_btc_eur}, {io,173,173,gdax_btc_eur}, {io,171,171,gdax_btc_eur}] Они быстро разгребаются, поэтому иногда она пустует. Текущая таблица ордеров: > length(kvs:all(order)). 1098 Скачать приложение: $ brew install erlang $ git clone git://github.com/spawnproc/ticker && cd ticker $ ./mad dep com pla rep Страница для вопросов и предложений — https://github.com/spawnproc/ticker